﻿// <copyright file="AbstractModPreferences.cs" company="SingularityShift">
// Copyright (c) 2009 SingularityShift.com
// </copyright>
// <author>Ben McClure</author>
// <email>ben@singularityshift.com</email>
// <date>2009-08-14</date>
// <summary>Implements most of the IModPreferences functionality for other classes to use</summary>
namespace GameLib.Games
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Xml;
    using System.Xml.Serialization;
    using GameLib.Util;
    using GameLib.Games.Util;
    using GameLib.Games.Components;

    /// <summary>
    /// <para>Describes a set of preferences regarding a single Mod for a game.</para>
    /// <para>This class provides the base functionality for any Mod Preferences class.</para>
    /// </summary>
    public abstract class AbstractModPreferences : IModPreferences
    {
        /// <summary>
        /// Gets or sets the backing Xml document for this IModPreferences object.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId = "System.Xml.XmlNode", Justification = "Can't think of a better way to expose the underlying DOM structure without needing to use XPath")]
        public virtual XmlDocument DomDocument
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the file path of the preferences file.
        /// </summary>
        public virtual string FilePath
        {
            get;
            set;
        }

        /// <summary>
        /// Gets a value indicating whether the FilePath is valid on this system.
        /// </summary>
        public virtual bool FilePathIsValid
        {
            get
            {
                if (String.IsNullOrEmpty(this.FilePath))
                {
                    return false;
                }
                else if (!Path.IsPathRooted(this.FilePath))
                {
                    return false;
                }
                else if (!Path.HasExtension(this.FilePath))
                {
                    return false;
                }
                else if (!Path.GetExtension(this.FilePath).Equals(".xml", StringComparison.CurrentCultureIgnoreCase))
                {
                    return false;
                }

                return true;
            }
        }

        /// <summary>
        /// <para>Gets a value indicating whether the Preferences file is currently loaded.</para>
        /// <para>Explicite loading is usually not necessary, as the file will be loaded on demand as needed, and cached in memory until no longer needed.</para>
        /// </summary>
        public virtual bool IsLoaded
        {
            get
            {
                return this.DomDocument != null;
            }
        }

        /// <summary>
        /// Gets or sets an integer representing the mod's rating. This is often 0-5, but the limits are defined by validation, which can be overridden.
        /// </summary>
        public virtual int Rating
        {
            get
            {
                string strval = this.GetPreferenceElement("rating").Value;
                if (String.IsNullOrEmpty(strval))
                {
                    return -1;
                }

                int val = int.Parse(strval, CultureInfo.CurrentCulture);
                if (ValidateRating(val))
                {
                    return val;
                }
                else
                {
                    return -1;
                }
            }

            set
            {
                if (!ValidateRating(value))
                {
                    throw new IntegerOutOfRangeException(value);
                }

                this.GetPreferenceElement("rating").Value = value.ToString(CultureInfo.CurrentCulture);
            }
        }

        /// <summary>
        /// Gets or sets the collection of conflict patterns to ignore when matching against this mod.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The collection is only a front-end for the XML elements backing it.")]
        public virtual IList<string> IgnoredConflictPatterns
        {
            get
            {
                IList<string> result = new Collection<string>();
                foreach (XmlElement node in this.GetPreferenceElement("ignored-conflict-patterns").GetElementsByTagName("ignored-conflict-pattern"))
                {
                    result.Add(node.Value);
                }

                return result;
            }

            set
            {
                if (value != null)
                {
                    foreach (string val in value)
                    {
                        XmlElement node = this.DomDocument.CreateElement("ignored-conflict-pattern");
                        node.Value = val;
                        this.GetPreferenceElement("ignored-conflict-patterns").AppendChild(node);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the collection of dependency patterns to ignore when matching against this mod.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The collection is only a front-end for the XML elements backing it.")]
        public virtual IList<string> IgnoredDependencyPatterns
        {
            get
            {
                IList<string> result = new Collection<string>();
                foreach (XmlElement node in this.GetPreferenceElement("ignored-dependency-patterns").GetElementsByTagName("ignored-dependency-pattern"))
                {
                    result.Add(node.Value);
                }

                return result;
            }

            set
            {
                if (value != null)
                {
                    foreach (string val in value)
                    {
                        XmlElement node = this.DomDocument.CreateElement("ignored-dependency-pattern");
                        node.Value = val;
                        this.GetPreferenceElement("ignored-dependency-patterns").AppendChild(node);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the collection of message patterns to ignore when matching against this mod.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The collection is only a front-end for the XML elements backing it.")]
        public virtual IList<string> IgnoredMessagePatterns
        {
            get
            {
                IList<string> result = new Collection<string>();
                foreach (XmlElement node in this.GetPreferenceElement("ignored-message-patterns").GetElementsByTagName("ignored-message-pattern"))
                {
                    result.Add(node.Value);
                }

                return result;
            }

            set
            {
                if (value != null)
                {
                    foreach (string val in value)
                    {
                        XmlElement node = this.DomDocument.CreateElement("ignored-message-pattern");
                        node.Value = val;
                        this.GetPreferenceElement("ignored-message-patterns").AppendChild(node);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the IModOrderingInformation provided for this mod.
        /// </summary>
        public virtual IModOrderingInformation OrderingInformation
        {
            get
            {
                XmlElement node = this.GetOrderingInformation();
                IModOrderingInformation orderingInfo = new ModOrderingInformation(node.GetElementsByTagName("group")[0].Value, int.Parse(node.GetElementsByTagName("index")[0].Value, CultureInfo.CurrentCulture), int.Parse(node.GetElementsByTagName("priority")[0].Value, CultureInfo.CurrentCulture));
                foreach (XmlElement element in node.GetElementsByTagName("load-before")[0].ChildNodes)
                {
                    if (element.NodeType.Equals(XmlNodeType.Element) && ((XmlElement)element).Name.Equals("pattern"))
                    {
                        orderingInfo.LoadBefore.Add(element.Value);
                    }
                }

                foreach (XmlElement element in node.GetElementsByTagName("load-after")[0].ChildNodes)
                {
                    if (element.NodeType.Equals(XmlNodeType.Element) && ((XmlElement)element).Name.Equals("pattern"))
                    {
                        orderingInfo.LoadAfter.Add(element.Value);
                    }
                }

                return orderingInfo;
            }

            set
            {
                if (value != null)
                {
                    XmlElement node = this.GetOrderingInformation();
                    node.GetElementsByTagName("group")[0].Value = value.Group;
                    node.GetElementsByTagName("index")[0].Value = value.Index.ToString(CultureInfo.CurrentCulture);
                    node.GetElementsByTagName("priority")[0].Value = value.Priority.ToString(CultureInfo.CurrentCulture);
                    XmlNode loadBefore = node.GetElementsByTagName("load-before")[0];
                    foreach (string str in value.LoadBefore)
                    {
                        XmlElement element = this.DomDocument.CreateElement("pattern");
                        element.Value = str;
                        loadBefore.AppendChild(element);
                    }

                    XmlNode loadAfter = node.GetElementsByTagName("load-after")[0];
                    foreach (string str in value.LoadAfter)
                    {
                        XmlElement element = this.DomDocument.CreateElement("pattern");
                        element.Value = str;
                        loadAfter.AppendChild(element);
                    }
                }
            }
        }

        /// <summary>
        /// <para>Determines if the provided rating is within the allowed range.</para>
        /// <para>This can be overridden to impose new validation rules on the rating in an implementing class.</para>
        /// </summary>
        /// <param name="value">The rating integer to validate.</param>
        /// <returns>A bool indicating whether this rating is valid or not.</returns>
        public static bool ValidateRating(int value)
        {
            int min = -1;
            int max = 5;
            if ((value < min) || (value > max))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Load the mod preferences from the FilePath property of the object.
        /// </summary>
        public virtual void Load()
        {
            if (this.FilePathIsValid)
            {
                FileStream stream = new FileStream(this.FilePath, FileMode.OpenOrCreate);
                this.DomDocument.Load(stream);
            }
        }

        /// <summary>
        /// Save the mod preferences to the FilePath property of the object.
        /// </summary>
        public virtual void Save()
        {
            if ((!this.IsLoaded) || String.IsNullOrEmpty(this.FilePath))
            {
                return;
            }

            this.DomDocument.Save(this.FilePath);
        }

        /// <summary>
        /// Gets the ordering information from the mod preferences XML node, creating it if necessary.
        /// </summary>
        /// <returns>An XmlElement referencing the ordering information node</returns>
        private XmlElement GetOrderingInformation()
        {
            XmlElement node = this.GetPreferenceElement("ordering-information");
            if (node.GetElementsByTagName("index")[0] == null)
            {
                node.AppendChild(this.DomDocument.CreateElement("index"));
            }

            if (node.GetElementsByTagName("group")[0] == null)
            {
                node.AppendChild(this.DomDocument.CreateElement("group"));
            }

            if (node.GetElementsByTagName("priority")[0] == null)
            {
                node.AppendChild(this.DomDocument.CreateElement("priority"));
            }

            if (node.GetElementsByTagName("load-before")[0] == null)
            {
                node.AppendChild(this.DomDocument.CreateElement("load-before"));
            }

            if (node.GetElementsByTagName("load-after")[0] == null)
            {
                node.AppendChild(this.DomDocument.CreateElement("load-after"));
            }

            return node;
        }

        /// <summary>
        /// Gets the XML preferences element with the specified name, creating it if necessary
        /// </summary>
        /// <param name="name">The name of the preferences element to get or create</param>
        /// <returns>An XmlElement for the specified preferences</returns>
        private XmlElement GetPreferenceElement(string name)
        {
            if (!this.IsLoaded)
            {
                this.Load();
            }

            XmlNodeList nodeList = this.DomDocument.DocumentElement.GetElementsByTagName(name);
            if (nodeList != null)
            {
                return (XmlElement)nodeList[0];
            }
            else
            {
                XmlElement node = this.DomDocument.CreateElement(name);
                this.DomDocument.DocumentElement.AppendChild(node);
                return node;
            }
        }
    }
}